<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**************************************************************************
 *
 *   Copyright 2010 American Public Media Group
 *
 *   This file is part of AIR2.
 *
 *   AIR2 is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   AIR2 is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with AIR2.  If not, see <http://www.gnu.org/licenses/>.
 *
 *************************************************************************/

/**
 * Air Query Arguments Library
 *
 * Contains methods to process query arguments (usually GET parameters), making
 * the proper modifications to a AIR2_Query object.  Additional parameters
 * can be added to the controller, and passed in via the AirQargs constructor.
 *
 * @author rcavis
 * @package default
 */
class AirQargs {
    /*
     * Array of 'arg_name' => 'method_name'.  Methods must exist either in this
     * class, or in the calling controller.  Multiple query args can be passed
     * to the same method by seperating them with a '|'.  The method will only
     * be called if AT LEAST 1 of the args was set, and NULL will be passed for
     * the remaining args.
     */
    private $valid_args = array(
        // searching
        'q|b'      => 'q_search',
        // sorting
        'sort|dir' => 'q_sort_single',
        's'        => 'q_sort_multi',
        // paging
        'start'    => 'q_offset',
        'o'        => 'q_offset',
        'limit'    => 'q_limit',
        'p'        => 'q_limit',
        // uuid-only query
        'u'        => 'q_uuid_only',
    );
    private $ctrlr = null; // cached reference to the calling controller
    public $sort_applied = array(); // track applied sorts


    /**
     * Constructor - load the Air Query Args library, optionally adding or
     * overriding some valid_args from the calling controller.
     *
     * @param assoc-array $params optional keys are ('valid_args')
     */
    public function AirQargs($params=null) {
        $this->ctrlr =& get_instance();

        // add (and override) valid args
        if (isset($params['valid_args']) && is_array($params['valid_args'])) {
            foreach ($params['valid_args'] as $name => $func) {
                $this->valid_args[$name] = $func;
            }
        }
    }


    /**
     * Search for valid query args in $args, and add them to the Doctrine Query.
     *
     * @param AIR2_Query $q    query to alter
     * @param assoc-array $args list of key-value arguments
     */
    public function process_args($q, $args) {
        // look for any valid args
        if (count($args) > 0) {
            foreach ($this->valid_args as $keys => $method) {
                $this->_process_arg($q, $args, $keys, $method);
            }
        }
    }


    /**
     * Search for a set of keys in the args, and call their associated methods
     *
     * @param AIR2_Query $q           query to alter
     * @param assoc-array $args        list of key-value arguments
     * @param string  $lookup_keys list of keys delimited by '|'
     * @param string  $method      function to call if keys are found
     */
    private function _process_arg($q, $args, $lookup_keys, $method) {
        $karr = explode('|', $lookup_keys);

        // get the arguments for this function
        $func_args = array($q);
        $call = false;
        foreach ($karr as $key) {
            if (isset($args[$key]) && !is_null($args[$key])) {
                $func_args[] = $args[$key];
                $call = true;
            }
            else {
                $func_args[] = null;
            }
        }

        // call the function, if necessary
        if ($call) {
            if (method_exists($this->ctrlr, $method)) {
                call_user_func_array(array($this->ctrlr, $method), $func_args);
            }
            elseif (method_exists($this, $method)) {
                call_user_func_array(array($this, $method), $func_args);
            }
        }
    }


    /**************************************************************************
     * QUERY ARG METHODS
     *   to process specific query args
     *************************************************************************/

    /**
     * Search for a string within this resource
     *
     * @param AIR2_Query $q    query to alter
     * @param string  $str  search for
     * @param string  $oper AND|OR
     */
    private function q_search($q, $str, $oper) {
        // set the use_or flag
        if ($oper == 'OR') $oper = true;
        else $oper = false;

        // look for search method in model class
        if (strlen($str) > 0) {
            $a = $q->getRootAlias();
            $t = $q->getRoot()->getClassnameToReturn();
            if (method_exists($t, 'add_search_str')) {
                call_user_func(array($t, 'add_search_str'), $q, $a, $str, $oper);
            }
        }
    }


    /**
     * Add ordering based on a single field
     *
     * @param AIR2_Query $q    query to alter
     * @param string  $sort field name to sort
     * @param string  $dir  ASC|DESC
     */
    private function q_sort_single($q, $sort, $dir) {
        if ($sort && strlen($sort) > 0) {
            // determine direction
            if ($dir != 'DESC') $dir = 'ASC';

            // if there's a "." in the sort field, assume that it's mapped to
            // a related table.  Try to find the alias for it.
            $q->getSqlQuery(); // trigger DQL parser
            $root = $q->getRoot();
            $root_alias = $q->getRootAlias();
            $cmps = $q->getQueryComponents();
            $fld = $this->_find_alias($root, $root_alias, $sort, $cmps);
            if ($fld) {
                $this->sort_applied[] = array($sort, $fld, $dir);
                $sort = $fld;
            }
            else {
                $this->sort_applied[] = array($sort, $sort, $dir);
            }

            $q->addOrderBy("$sort $dir");
        }
    }


    /**
     * Finds the alias for a column/field, given a field name containing
     * relationship names.  For instance, for a Source query you might pass in
     * $fld = 'CreUser.user_first_name'.  This would return the query alias
     * for Source->CreUser->user_first_name.
     *
     * @param Doctrine_Table $parent
     * @param string  $parent_alias
     * @param string  $fld
     * @param array   $cmps         components of the query
     * @return string
     */
    private function _find_alias($parent, $parent_alias, $fld, $cmps) {
        if ($parent->hasField($fld)) return "$parent_alias.$fld";

        $parts = explode('.', $fld, 2);
        if (count($parts) > 1 && $parent->hasRelation($parts[0])) {
            // find alias for THIS relation in the cmps
            $r = $parent->getRelation($parts[0]);
            foreach ($cmps as $a => $comp) {
                if (isset($comp['parent']) && $comp['parent'] == $parent_alias
                    && $comp['relation'] == $r) {
                    // try to find the alias within this comp
                    $res = $this->_find_alias($comp['table'], $a, $parts[1], $cmps);
                    if ($res) return $res;
                }
            }
        }

        return null; //alias not found
    }


    /**
     * Sort multiple fields based on a valid sort-string, having the format:
     * "field1 ASC, field2 DESC, field3 ASC"
     *
     * @param AIR2_Query $q    query to alter
     * @param string  $sort one or more sort definitions
     */
    private function q_sort_multi($q, $sort) {
        $sarr = explode(',', $sort);
        foreach ($sarr as $s) {
            $s = explode(' ', trim($s));
            if (count($s) > 0) {
                if (!isset($s[1])) $s[1] = null;
                $this->q_sort_single($q, $s[0], $s[1]);
            }
        }
    }


    /**
     * Offset the beginning of a result set
     *
     * @param AIR2_Query $q      query to alter
     * @param int     $offset integer offset
     */
    private function q_offset($q, $offset) {
        if (is_numeric($offset) && $offset > 0) {
            $q->offset($offset);
        }
    }


    /**
     * Limit the number of items returned in a result set
     *
     * @param AIR2_Query $q     query to alter
     * @param int     $limit integer limit
     */
    private function q_limit($q, $limit) {
        if (is_numeric($limit) && $limit > 0) {
            $q->limit($limit);
        }
    }


    /**
     * Return only the base-object (no joins)
     *
     * @param AIR2_Query $q    query to alter
     * @param int|bool $flag 0|1|true|false
     */
    private function q_uuid_only($q, $flag) {
        if ($flag == 1 || $flag == true) {
            $a = $q->getRootAlias();
            $q->select("$a.*");
        }
    }


}
